// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "textcon.h"

#include <assert.h>
#include <string.h>
#include <zircon/compiler.h>

#include <fbl/algorithm.h>

static inline void invalidate(textcon_t* tc, int x, int y, int w, int h) {
  tc->invalidate(tc->cookie, x, y, w, h);
}
static inline void movecursor(textcon_t* tc, int x, int y) { tc->movecursor(tc->cookie, x, y); }
static inline void push_scrollback_line(textcon_t* tc, int y) {
  tc->push_scrollback_line(tc->cookie, y);
}
static inline void setparam(textcon_t* tc, int param, uint8_t* arg, size_t arglen) {
  tc->setparam(tc->cookie, param, arg, arglen);
}

// Construct a vc_char_t from the given character using the current colors.
static inline vc_char_t make_vc_char(textcon_t* tc, uint8_t ch) {
  return (vc_char_t)(ch | (((vc_char_t)tc->fg & 15) << 8) | (((vc_char_t)tc->bg & 15) << 12));
}

// Note that x coordinates are allowed to be one-past-the-end, as
// described in textcon.h.
static vc_char_t* dataxy(textcon_t* tc, int x, int y) {
  assert(x >= 0);
  // In particular, this assertion is <= to allow the one-past-the-end.
  assert(x <= tc->w);
  assert(y >= 0);
  assert(y < tc->h);
  return tc->data + y * tc->w + x;
}

static vc_char_t* get_start_of_line(textcon_t* tc, int y) {
  assert(y >= 0);
  assert(y <= tc->h);
  return tc->data + y * tc->w;
}

static int clampx(textcon_t* tc, int x) { return x < 0 ? 0 : x >= tc->w ? tc->w - 1 : x; }

static int clampxatedge(textcon_t* tc, int x) { return x < 0 ? 0 : x > tc->w ? tc->w : x; }

static int clampy(textcon_t* tc, int y) { return y < 0 ? 0 : y >= tc->h ? tc->h - 1 : y; }

static void moveto(textcon_t* tc, int x, int y) {
  tc->x = clampx(tc, x);
  tc->y = clampy(tc, y);
}

static inline void moverel(textcon_t* tc, int dx, int dy) { moveto(tc, tc->x + dx, tc->y + dy); }

static void fill(vc_char_t* ptr, vc_char_t val, size_t count) {
  while (count-- > 0) {
    *ptr++ = val;
  }
}

static void erase_region(textcon_t* tc, int x0, int y0, int x1, int y1) {
  if (x0 >= tc->w) {
    return;
  }
  x1 = clampx(tc, x1);
  vc_char_t* ptr = dataxy(tc, x0, y0);
  vc_char_t* end = dataxy(tc, x1, y1) + 1;
  fill(ptr, make_vc_char(tc, ' '), end - ptr);
  invalidate(tc, x0, y0, x1 - x0 + 1, y1 - y0 + 1);
}

static void erase_screen(textcon_t* tc, int arg) {
  switch (arg) {
    case 0:  // erase downward
      erase_region(tc, tc->x, tc->y, tc->w - 1, tc->h - 1);
      break;
    case 1:  // erase upward
      erase_region(tc, 0, 0, tc->x, tc->y);
      break;
    case 2:  // erase all
      erase_region(tc, 0, 0, tc->w - 1, tc->h - 1);
      break;
  }
}

static void erase_line(textcon_t* tc, int arg) {
  switch (arg) {
    case 0:  // erase to eol
      erase_region(tc, tc->x, tc->y, tc->w - 1, tc->y);
      break;
    case 1:  // erase from bol
      erase_region(tc, 0, tc->y, tc->x, tc->y);
      break;
    case 2:  // erase line
      erase_region(tc, 0, tc->y, tc->w - 1, tc->y);
      break;
  }
}

static void erase_chars(textcon_t* tc, int arg) {
  if (tc->x >= tc->w) {
    return;
  }
  if (arg < 0) {
    arg = 0;
  }
  int x_erase_end = fbl::min(tc->x + arg, tc->w);

  vc_char_t* dst = dataxy(tc, tc->x, tc->y);
  vc_char_t* src = dataxy(tc, x_erase_end, tc->y);
  vc_char_t* end = dataxy(tc, tc->w, tc->y);

  while (src < end) {
    *dst++ = *src++;
  }
  while (dst < end) {
    *dst++ = make_vc_char(tc, ' ');
  }

  invalidate(tc, tc->x, tc->y, tc->w - tc->x, 1);
}

void tc_copy_lines(textcon_t* tc, int y_dest, int y_src, int line_count) {
  vc_char_t* dest = get_start_of_line(tc, y_dest);
  vc_char_t* src = get_start_of_line(tc, y_src);
  memmove(dest, src, line_count * tc->w * sizeof(vc_char_t));
}

static void clear_lines(textcon_t* tc, int y, int line_count) {
  fill(get_start_of_line(tc, y), make_vc_char(tc, ' '), line_count * tc->w);
  invalidate(tc, 0, y, tc->w, line_count);
}

// Scroll the region between line |y0| (inclusive) and |y1| (exclusive).
// Scroll by |diff| lines, which may be positive (for moving lines up) or
// negative (for moving lines down).
static void scroll_lines(textcon_t* tc, int y0, int y1, int diff) {
  int delta = diff > 0 ? diff : -diff;
  if (delta > y1 - y0)
    delta = y1 - y0;
  int copy_count = y1 - y0 - delta;
  if (diff > 0) {
    // Scroll up.
    for (int i = 0; i < delta; ++i) {
      push_scrollback_line(tc, y0 + i);
    }
    tc->copy_lines(tc->cookie, y0, y0 + delta, copy_count);
    clear_lines(tc, y0 + copy_count, delta);
  } else {
    // Scroll down.
    tc->copy_lines(tc->cookie, y0 + delta, y0, copy_count);
    clear_lines(tc, y0, delta);
  }
}

static void scroll_up(textcon_t* tc) { scroll_lines(tc, tc->scroll_y0, tc->scroll_y1, 1); }

// positive = up, negative = down
static void scroll_at_pos(textcon_t* tc, int dir) {
  if (tc->y < tc->scroll_y0)
    return;
  if (tc->y >= tc->scroll_y1)
    return;

  scroll_lines(tc, tc->y, tc->scroll_y1, dir);
}

void set_scroll(textcon_t* tc, int y0, int y1) {
  if (y0 > y1) {
    return;
  }
  tc->scroll_y0 = (y0 < 0) ? 0 : y0;
  tc->scroll_y1 = (y1 > tc->h) ? tc->h : y1;
}

static void savecursorpos(textcon_t* tc) {
  tc->save_x = tc->x;
  tc->save_y = tc->y;
}

static void restorecursorpos(textcon_t* tc) {
  tc->x = clampxatedge(tc, tc->save_x);
  tc->y = clampy(tc, tc->save_y);
}

static void putc_plain(textcon_t* tc, uint8_t c);
static void putc_escape2(textcon_t* tc, uint8_t c);

static void putc_ignore(textcon_t* tc, uint8_t c) { tc->putc = putc_plain; }

static void putc_param(textcon_t* tc, uint8_t c) {
  switch (c) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      tc->num = tc->num * 10 + (c - '0');
      return;
    case ';':
      if (tc->argn_count < TC_MAX_ARG) {
        tc->argn[tc->argn_count++] = tc->num;
      }
      tc->putc = putc_escape2;
      break;
    default:
      if (tc->argn_count < TC_MAX_ARG) {
        tc->argn[tc->argn_count++] = tc->num;
      }
      tc->putc = putc_escape2;
      putc_escape2(tc, c);
      break;
  }
}

#define ARG0(def) ((tc->argn_count > 0) ? tc->argn[0] : (def))
#define ARG1(def) ((tc->argn_count > 1) ? tc->argn[1] : (def))

static void putc_dec(textcon_t* tc, uint8_t c) {
  switch (c) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      tc->num = tc->num * 10 + (c - '0');
      return;
    case 'h':
      if (tc->num == 25)
        setparam(tc, TC_SHOW_CURSOR, NULL, 0);
      break;
    case 'l':
      if (tc->num == 25)
        setparam(tc, TC_HIDE_CURSOR, NULL, 0);
      break;
    default:
      putc_plain(tc, c);
      break;
  }
  tc->putc = putc_plain;
}

static textcon_param_t osc_to_param(int osc) {
  switch (osc) {
    case 2:
      return TC_SET_TITLE;
    default:
      return TC_INVALID;
  }
}

static void putc_osc2(textcon_t* tc, uint8_t c) {
  switch (c) {
    case 7: {  // end command
      textcon_param_t param = osc_to_param(ARG0(-1));
      if (param != TC_INVALID && tc->argstr_size)
        setparam(tc, param, tc->argstr, tc->argstr_size);
      tc->putc = putc_plain;
      break;
    }
    default:
      if (tc->argstr_size < TC_MAX_ARG_LENGTH)
        tc->argstr[tc->argstr_size++] = c;
      break;
  }
}

static void putc_osc(textcon_t* tc, uint8_t c) {
  switch (c) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      tc->num = tc->num * 10 + (c - '0');
      return;
    case ';':
      if (tc->argn_count < TC_MAX_ARG) {
        tc->argn[tc->argn_count++] = tc->num;
      }
      memset(tc->argstr, 0, TC_MAX_ARG_LENGTH);
      tc->argstr_size = 0;
      tc->putc = putc_osc2;
      break;
    default:
      if (tc->argn_count < TC_MAX_ARG) {
        tc->argn[tc->argn_count++] = tc->num;
      }
      tc->putc = putc_osc2;
      putc_osc2(tc, c);
      break;
  }
}

static void putc_escape2(textcon_t* tc, uint8_t c) {
  int x, y;
  switch (c) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      tc->num = c - '0';
      tc->putc = putc_param;
      return;
    case ';':  // end parameter
      if (tc->argn_count < TC_MAX_ARG) {
        tc->argn[tc->argn_count++] = 0;
      }
      return;
    case '?':
      tc->num = 0;
      tc->argn_count = 0;
      tc->putc = putc_dec;
      return;
    case 'A':  // (CUU) Cursor Up
      moverel(tc, 0, -ARG0(1));
      break;
    case 'B':  // (CUD) Cursor Down
      moverel(tc, 0, ARG0(1));
      break;
    case 'C':  // (CUF) Cursor Forward
      moverel(tc, ARG0(1), 0);
      break;
    case 'D':  // (CUB) Cursor Backward
      moverel(tc, -ARG0(1), 0);
      break;
    case 'E':
      moveto(tc, 0, tc->y + ARG0(1));
      break;
    case 'F':
      moveto(tc, 0, tc->y - ARG0(1));
      break;
    case 'G':  // move xpos absolute
      x = ARG0(1);
      moveto(tc, x ? (x - 1) : 0, tc->y);
      break;
    case 'H':  // (CUP) Cursor Position
    case 'f':  // (HVP) Horizontal and Vertical Position
      x = ARG1(1);
      y = ARG0(1);
      moveto(tc, x ? (x - 1) : 0, y ? (y - 1) : 0);
      break;
    case 'J':  // (ED) erase in display
      erase_screen(tc, ARG0(0));
      break;
    case 'K':  // (EL) erase in line
      erase_line(tc, ARG0(0));
      break;
    case 'L':  // (IL) insert line(s) at cursor
      scroll_at_pos(tc, -ARG0(1));
      break;
    case 'M':  // (DL) delete line(s) at cursor
      scroll_at_pos(tc, ARG0(1));
      break;
    case 'P':  // (DCH) delete character(s)
      erase_chars(tc, ARG0(1));
      break;
    case 'd':  // move ypos absolute
      y = ARG0(1);
      moveto(tc, tc->x, y ? (y - 1) : 0);
      break;
    case 'm':                     // (SGR) Character Attributes
      if (tc->argn_count == 0) {  // no params == default param
        tc->argn[0] = 0;
        tc->argn_count = 1;
      }
      for (int i = 0; i < tc->argn_count; i++) {
        int n = tc->argn[i];
        if ((n >= 30) && (n <= 37)) {  // set fg
          tc->fg = (uint8_t)(n - 30);
        } else if ((n >= 40) && (n <= 47)) {  // set bg
          tc->bg = (uint8_t)(n - 40);
        } else if ((n == 1) && (tc->fg <= 7)) {  // bold
          tc->fg = (uint8_t)(tc->fg + 8);
        } else if (n == 0) {  // reset
          tc->fg = tc->init_fg;
          tc->bg = tc->init_bg;
        } else if (n == 7) {  // reverse
          uint8_t temp = tc->fg;
          tc->fg = tc->bg;
          tc->bg = temp;
        } else if (n == 39) {  // default fg
          tc->fg = tc->init_fg;
        } else if (n == 49) {  // default bg
          tc->bg = tc->init_bg;
        }
      }
      break;
    case 'r':  // set scroll region
      set_scroll(tc, ARG0(1) - 1, ARG1(tc->h));
      break;
    case 's':  // save cursor position ??
      savecursorpos(tc);
      break;
    case 'u':  // restore cursor position ??
      restorecursorpos(tc);
      break;
    case '@':  // (ICH) Insert Blank Character(s)
    case 'T':  // Initiate Hilight Mouse Tracking (xterm)
    case 'c':  // (DA) Send Device Attributes
    case 'g':  // (TBC) Tab Clear
    case 'h':  // (SM) Set Mode  (4=Insert,20=AutoNewline)
    case 'l':  // (RM) Reset Mode (4=Replace,20=NormalLinefeed)
    case 'n':  // (DSR) Device Status Report
    case 'x':  // Request Terminal Parameters
    default:
      break;
  }
  movecursor(tc, tc->x, tc->y);
  tc->putc = putc_plain;
}

static void putc_escape(textcon_t* tc, uint8_t c) {
  switch (c) {
    case 27:  // escape
      return;
    case '(':
    case ')':
    case '*':
    case '+':
      // select various character sets
      tc->putc = putc_ignore;
      return;
    case '[':
      tc->num = 0;
      tc->argn_count = 0;
      tc->putc = putc_escape2;
      return;
    case ']':
      tc->num = 0;
      tc->argn_count = 0;
      tc->putc = putc_osc;
      return;
    case '7':  // (DECSC) Save Cursor
      savecursorpos(tc);
      // save attribute
      break;
    case '8':  // (DECRC) Restore Cursor
      restorecursorpos(tc);
      movecursor(tc, tc->x, tc->y);
      break;
    case 'E':  // (NEL) Next Line
      tc->x = 0;
      __FALLTHROUGH;
    case 'D':  // (IND) Index
      tc->y++;
      if (tc->y >= tc->scroll_y1) {
        tc->y--;
        scroll_up(tc);
      }
      movecursor(tc, tc->x, tc->y);
      break;
    case 'M':  // (RI) Reverse Index)
      tc->y--;
      if (tc->y < tc->scroll_y0) {
        tc->y++;
        scroll_at_pos(tc, -1);
      }
      movecursor(tc, tc->x, tc->y);
      break;
  }
  tc->putc = putc_plain;
}

static void putc_cr(textcon_t* tc) { tc->x = 0; }

static void putc_lf(textcon_t* tc) {
  tc->y++;
  if (tc->y >= tc->scroll_y1) {
    tc->y--;
    scroll_up(tc);
  }
}

static void putc_plain(textcon_t* tc, uint8_t c) {
  switch (c) {
    case 7:  // bell
      break;
    case 8:  // backspace / ^H
      if (tc->x > 0)
        tc->x--;
      break;
    case 9:  // tab / ^I
      moveto(tc, (tc->x + 8) & (~7), tc->y);
      break;
    case 10:        // newline
      putc_cr(tc);  // should we imply this?
      putc_lf(tc);
      break;
    case 12:
      erase_screen(tc, 2);
      break;
    case 13:  // carriage return
      putc_cr(tc);
      break;
    case 27:  // escape
      tc->putc = putc_escape;
      return;
    default:
      if ((c < ' ') || (c > 127)) {
        return;
      }
      if (tc->x >= tc->w) {
        // apply deferred line wrap upon printing first character beyond
        // end of current line
        putc_cr(tc);
        putc_lf(tc);
      }
      dataxy(tc, tc->x, tc->y)[0] = make_vc_char(tc, c);
      invalidate(tc, tc->x, tc->y, 1, 1);
      tc->x++;
      break;
  }
  movecursor(tc, tc->x, tc->y);
}

void tc_init(textcon_t* tc, int w, int h, vc_char_t* data, uint8_t fg, uint8_t bg, int cursor_x,
             int cursor_y) {
  tc->w = w;
  tc->h = h;
  tc->x = cursor_x;
  tc->y = cursor_y;
  tc->data = data;
  tc->scroll_y0 = 0;
  tc->scroll_y1 = h;
  tc->save_x = 0;
  tc->save_y = 0;
  tc->fg = fg;
  tc->bg = bg;
  tc->init_fg = fg;
  tc->init_bg = bg;
  tc->putc = putc_plain;
}

void tc_seth(textcon_t* tc, int h) {
  // tc->data must be big enough for the new height
  int old_h = h;
  tc->h = h;

  // move contents
  int y = 0;
  if (old_h > h) {
    vc_char_t* dst = dataxy(tc, 0, tc->scroll_y0);
    vc_char_t* src = dataxy(tc, 0, tc->scroll_y0 + old_h - h);
    vc_char_t* end = dataxy(tc, 0, tc->scroll_y1);
    do {
      push_scrollback_line(tc, y);
    } while (++y < old_h - h);
    memmove(dst, src, (end - dst) * sizeof(vc_char_t));
    tc->y -= old_h - h;
  } else if (old_h < h) {
    do {
      fill(dataxy(tc, 0, tc->scroll_y1 + y), make_vc_char(tc, ' '), tc->w);
    } while (++y < h - old_h);
  }
  tc->y = clampy(tc, tc->y);

  // try to fixup the scroll region
  if (tc->scroll_y0 >= h) {
    tc->scroll_y0 = 0;
  }
  if (tc->scroll_y1 == old_h) {
    tc->scroll_y1 = h;
  } else {
    tc->scroll_y1 = tc->scroll_y1 >= h ? h : tc->scroll_y1;
  }

  invalidate(tc, 0, 0, tc->w, tc->h);
  movecursor(tc, tc->x, tc->y);
}
